package com.rabincorp.euro2012.support;

import java.io.IOException;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import com.rabincorp.euro2012.support.UserSession.Type;

@SuppressWarnings("serial")
abstract public class DispatchServlet extends HttpServlet {
	
	private static final Logger logger = Logger.getLogger("com.rabincorp.euro2012.support.DispatchServlet");
	
	// these two maps are configured upon servlet init()
	// and will not be change after that
	private Map<String, Service> serviceMapGET = new HashMap<String, Service>();
	private Map<String, Service> serviceMapPOST = new HashMap<String, Service>();

	abstract protected Collection<? extends Class<? extends Service>> register();

	private EntityManagerFactory entityManagerFactory = Persistence
			.createEntityManagerFactory("transactions-optional");

	@Override
	public void init() throws ServletException {
		super.init();

		for (Class<? extends Service> serviceClass : register()) {

			ServiceConfig config = serviceClass
					.getAnnotation(ServiceConfig.class);

			if (config == null) {
				throw new ServletException(serviceClass
						+ " must be annotated with ServiceConfig");
			}

			Map<String, Service> map = config.method() == HttpMethod.GET ? serviceMapGET
					: serviceMapPOST;
			String path = "/" + config.path();

			if (map.containsKey(path)) {
				throw new ServletException(serviceClass + " and "
						+ map.get(path).getClass() + " both mapped to " + path);
			}

			try {
				map.put(path, serviceClass.newInstance());
			} catch (Exception e) {
				throw new ServletException(e);
			}

			logger.info(serviceClass + " mapped to " + path);
		}
	}

	@Override
	protected void doPost(HttpServletRequest req, HttpServletResponse resp)
			throws ServletException, IOException {
		serve(req, resp, serviceMapPOST.get(req.getPathInfo()));

	}

	@Override
	protected void doGet(HttpServletRequest req, HttpServletResponse resp)
			throws ServletException, IOException {
		serve(req, resp, serviceMapGET.get(req.getPathInfo()));
	}

	private void serve(HttpServletRequest req, HttpServletResponse resp,
			Service service) throws ServletException, IOException {

		if (service == null) {
			logger.info("no mapping found for " + req.getPathInfo());
			resp.sendError(HttpServletResponse.SC_NOT_FOUND);
			return;
		}

		ServiceContextImpl ctx = new ServiceContextImpl(req, resp);

		try {
			req.setAttribute("userSession", ctx.getUserSession());
			String view = service.serve(ctx);
			ctx.commitTransaction();

			if (view != null) {

				String redirectPrefix = "redirect:";

				if (view.startsWith(redirectPrefix)) {
					resp.sendRedirect(view.substring(redirectPrefix.length()));
				} else {
					req.getRequestDispatcher("/WEB-INF/jsp/" + view + ".jsp")
							.forward(req, resp);
				}

			}

		} catch (Exception e) {

			ctx.rollbackTransaction();

			if (e instanceof ServiceException) {
				req.setAttribute("error", e.getMessage());
				req.getRequestDispatcher("/WEB-INF/jsp/error.jsp").forward(req,
						resp);
			} else {
				logger.log(Level.SEVERE, "", e);

				req.setAttribute("error", "INTERNAL_ERR");
				req.getRequestDispatcher("/WEB-INF/jsp/error.jsp").forward(req,
						resp);
			}
		} finally {
			ctx.close();
		}

	}

	private class ServiceContextImpl implements ServiceContext {

		private HttpServletRequest request;
		private HttpServletResponse response;

		private EntityManager entityManager;
		private EntityTransaction transaction;
		private UserSession userSession;

		ServiceContextImpl(HttpServletRequest request,
				HttpServletResponse response) {
			super();
			this.request = request;
			this.response = response;
		}

		public void commitTransaction() {

			if (transaction != null && transaction.isActive()) {
				transaction.commit();
			}
		}

		@Override
		public HttpServletRequest getRequest() {
			return request;
		}

		@Override
		public HttpServletResponse getResponse() {
			return response;
		}

		@Override
		public UserSession getUserSession() {
			if (userSession != null) {
				return userSession;
			}
			HttpSession session = request.getSession(true);

			Long id = (Long) session.getAttribute("user.id");
			UserSession.Type type = (Type) session.getAttribute("user.type");

			userSession = new UserSession(id, type, getEntityManager());
			return userSession;
		}

		@Override
		public EntityManager getEntityManager() {

			if (entityManager == null) {
				entityManager = entityManagerFactory.createEntityManager();
				transaction = entityManager.getTransaction();
				transaction.begin();
			}

			return entityManager;
		}

		void rollbackTransaction() {
			if (transaction != null && transaction.isActive()) {
				try {
					transaction.rollback();
				} catch (Exception e) {
					logger.log(Level.WARNING, "", e);
				}
			}
		}

		void close() {

			if (entityManager != null && entityManager.isOpen()) {
				try {
					entityManager.close();
				} catch (Exception e) {
					logger.log(Level.WARNING, "", e);
				}
			}

		}
	}
}
